use glutin::{
    config::ConfigTemplateBuilder,
    context::{ContextApi, ContextAttributesBuilder},
    display::GetGlDisplay,
    prelude::{GlDisplay, NotCurrentGlContext},
    surface::{SurfaceAttributesBuilder, WindowSurface},
};

use glium::{
    Display,
    winit::{
        application::ApplicationHandler,
        dpi::PhysicalSize,
        event::{ElementState, KeyEvent, WindowEvent},
        event_loop::ActiveEventLoop,
        keyboard,
        raw_window_handle::HasWindowHandle,
        window::{Window, WindowId},
    },
};
use glutin_winit::DisplayBuilder;

pub trait WindowApp {
    fn draw_frame(&mut self, _display: &Display<WindowSurface>) {}
    fn update(&mut self) {}
    fn handle_window_event(&mut self, _event: &WindowEvent, _window: &Window) {}
}

pub struct AppState<T> {
    pub display: Display<WindowSurface>,
    pub window: Window,
    pub context: T,
}

type ContextCreator<T> = Box<dyn FnOnce(&Display<WindowSurface>) -> T>;

pub struct App<T> {
    pub state: Option<AppState<T>>,
    pub window_name: String,
    pub window_init_size: (usize, usize),
    pub context_creator: Option<ContextCreator<T>>,
}

impl<T: WindowApp> App<T> {
    pub fn new(
        creator: ContextCreator<T>,
        window_name: &str,
        window_init_size: (usize, usize),
    ) -> Self {
        Self {
            state: None,
            window_name: window_name.to_string(),
            window_init_size,
            context_creator: Some(creator),
        }
    }

    pub fn create_context(&mut self, event_loop: &ActiveEventLoop) {
        let window_attributes = Window::default_attributes()
            .with_title(&self.window_name)
            .with_visible(true)
            .with_inner_size(PhysicalSize::new(
                self.window_init_size.0 as u32,
                self.window_init_size.1 as u32,
            ));

        let config_template_builder = ConfigTemplateBuilder::new();
        let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attributes));

        // First we create a window
        let (window, gl_config) = display_builder
            .build(event_loop, config_template_builder, |mut configs| {
                // Just use the first configuration since we don't have any special preferences here
                configs.next().unwrap()
            })
            .unwrap();
        let window = window.unwrap();

        // Then the configuration which decides which OpenGL version we'll end up using, here we just use the default which is currently 3.3 core
        // When this fails we'll try and create an ES context, this is mainly used on mobile devices or various ARM SBC's
        // If you depend on features available in modern OpenGL Versions you need to request a specific, modern, version. Otherwise things will very likely fail.
        let window_handle = window
            .window_handle()
            .expect("couldn't obtain window handle");
        let context_attributes = ContextAttributesBuilder::new().build(Some(window_handle.into()));
        let fallback_context_attributes = ContextAttributesBuilder::new()
            .with_context_api(ContextApi::Gles(None))
            .build(Some(window_handle.into()));

        let not_current_gl_context = unsafe {
            gl_config
                .display()
                .create_context(&gl_config, &context_attributes)
                .unwrap_or_else(|_| {
                    gl_config
                        .display()
                        .create_context(&gl_config, &fallback_context_attributes)
                        .expect("failed to create context")
                })
        };

        // Determine our framebuffer size based on the window size, or default to 800x600 if it's invisible
        let (width, height): (u32, u32) = window.inner_size().into();
        let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
            window_handle.into(),
            std::num::NonZeroU32::new(width).unwrap(),
            std::num::NonZeroU32::new(height).unwrap(),
        );
        // Now we can create our surface, use it to make our context current and finally create our display
        let surface = unsafe {
            gl_config
                .display()
                .create_window_surface(&gl_config, &attrs)
                .unwrap()
        };
        let current_context = not_current_gl_context.make_current(&surface).unwrap();
        let display = Display::from_context_surface(current_context, surface).unwrap();

        let context = self.context_creator.take().unwrap()(&display);

        self.state = Some(AppState {
            display,
            window,
            context,
        })
    }
}

impl<T: WindowApp> ApplicationHandler<()> for App<T> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.state.is_none() {
            if self.context_creator.is_none() {
                event_loop.exit();
            } else {
                self.create_context(event_loop);
            }
        }
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        match event {
            WindowEvent::Resized(new_size) => {
                if let Some(state) = &mut self.state {
                    state.display.resize(new_size.into());
                }
            }
            WindowEvent::RedrawRequested => {
                if let Some(state) = &mut self.state {
                    state.context.update();
                    state.context.draw_frame(&state.display);
                }
            }
            WindowEvent::CloseRequested
            | WindowEvent::KeyboardInput {
                event:
                    KeyEvent {
                        state: ElementState::Pressed,
                        logical_key: keyboard::Key::Named(keyboard::NamedKey::Escape),
                        ..
                    },
                ..
            } => event_loop.exit(),
            _ => {}
        }

        if let Some(state) = &mut self.state {
            state.context.handle_window_event(&event, &state.window);
        }
    }

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        if let Some(state) = &mut self.state {
            state.window.request_redraw();
        }
    }
}
